{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "_2s0EJ5Fy4u2"
   },
   "source": [
    "# Week 2: Implementing Callbacks in TensorFlow using the MNIST Dataset\n",
    "\n",
    "In the lectures you learned how to do classification using Fashion MNIST, a dataset containing items of clothing. There's another, similar dataset called MNIST which has items of handwriting -- the digits 0 through 9.\n",
    "\n",
    "In this assignment you will code a classifier for the MNIST dataset, that trains until it reaches 98% accuracy and stops once this threshold is achieved. In the lectures you saw how this was done for the loss but here you will be using accuracy instead.\n",
    "\n",
    "Some notes:\n",
    "1. Your network should succeed in less than 10 epochs.\n",
    "2. When it reaches 98% or greater it should print out the string \"Reached 98% accuracy so cancelling training!\" and stop training."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### TIPS FOR SUCCESSFUL GRADING OF YOUR ASSIGNMENT:\n",
    "\n",
    "- All cells are frozen except for the ones where you need to submit your solutions or when explicitly mentioned you can interact with it.\n",
    "\n",
    "- You can add new cells to experiment but these will be omitted by the grader, so don't rely on newly created cells to host your solution code, use the provided places for this.\n",
    "\n",
    "- You can add the comment # grade-up-to-here in any graded cell to signal the grader that it must only evaluate up to that point. This is helpful if you want to check if you are on the right track even if you are not done with the whole assignment. Be sure to remember to delete the comment afterwards!\n",
    "\n",
    "- Avoid using global variables unless you absolutely have to. The grader tests your code in an isolated environment without running all cells from the top. As a result, global variables may be unavailable when scoring your submission. Global variables that are meant to be used will be defined in UPPERCASE.\n",
    "\n",
    "- To submit your notebook, save it and then click on the blue submit button at the beginning of the page."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "editable": false,
    "id": "djVOgMHty4u3",
    "tags": [
     "graded"
    ]
   },
   "outputs": [],
   "source": [
    "import os\n",
    "import base64\n",
    "import tensorflow as tf"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "editable": false,
    "tags": []
   },
   "outputs": [],
   "source": [
    "import unittests"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Load and inspect the data\n",
    "\n",
    "Begin by loading the data. A couple of things to notice:\n",
    "\n",
    "- The file `mnist.npz` is already included in the current workspace under the `data` directory. By default the `load_data` from Keras accepts a path relative to `~/.keras/datasets` but in this case it is stored somewhere else, as a result of this, you need to specify the full path.\n",
    "\n",
    "- `tf.keras.datasets.mnist.load_data` returns the train and test sets in the form of the tuples `(training_images, training_labels), (testing_images, testing_labels)` but in this exercise you will be needing only the train set so you can ignore the second tuple."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "editable": false,
    "tags": [
     "graded"
    ]
   },
   "outputs": [],
   "source": [
    "# Get current working directory\n",
    "current_dir = os.getcwd()\n",
    "\n",
    "# Append data/mnist.npz to the previous path to get the full path\n",
    "data_path = os.path.join(current_dir, \"data/mnist.npz\")\n",
    "\n",
    "# Load data (discard test set)\n",
    "(training_images, training_labels), _ = tf.keras.datasets.mnist.load_data(path=data_path)\n",
    "\n",
    "print(f\"training_images is of type {type(training_images)}.\\ntraining_labels is of type {type(training_labels)}\\n\")\n",
    "\n",
    "# Inspect shape of the data\n",
    "data_shape = training_images.shape\n",
    "\n",
    "print(f\"There are {data_shape[0]} examples with shape ({data_shape[1]}, {data_shape[2]})\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "One important step is to normalize the pixel values. The dataset includes black and white images and the pixel values for these kinds of images usually range from 0 to 255 but the network will have an easier time learning if these values range from 0 to 1.\n",
    "\n",
    "The data comes as numpy arrays so you can easily normalize the pixel values by using vectorization:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "editable": false,
    "tags": [
     "graded"
    ]
   },
   "outputs": [],
   "source": [
    "# Normalize pixel values\n",
    "training_images = training_images / 255.0"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise 1: create_and_compile_model\n",
    "\n",
    "Your first task is to create and compile the model that you will later train to recognize handwritten digits.\n",
    "\n",
    "Feel free to try the architecture for the neural network that you see fit but in case you need extra help you can check out an architecture that works pretty well at the end of this notebook. Notice that the part where the model is compiled is already provided (and the `accuracy` metric is defined so it can be accessed by your callback later on) so you only need to specify the layers of the model.\n",
    "\n",
    "Hints:\n",
    "- The first layer should take into consideration the `input_shape` of the data, which in this case is the size of each image\n",
    "- The last layer should take into account the number of classes you are trying to predict"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "tags": [
     "graded"
    ]
   },
   "outputs": [],
   "source": [
    "# GRADED FUNCTION: create_and_compile_model\n",
    "\n",
    "def create_and_compile_model():\n",
    "    \"\"\"Returns the compiled (but untrained) model.\n",
    "\n",
    "    Returns:\n",
    "        tf.keras.Model: The model that will be trained to predict predict handwriting digits.\n",
    "    \"\"\"\n",
    "\n",
    "    ### START CODE HERE ###\n",
    "        \n",
    "    # Define the model\n",
    "    model = tf.keras.models.Sequential([ \n",
    "\t\tNone\n",
    "    ]) \n",
    "\n",
    "    ### END CODE HERE ###\n",
    "    \n",
    "    # Compile the model\n",
    "    model.compile(\n",
    "\t\toptimizer='adam',\n",
    "\t\tloss='sparse_categorical_crossentropy',\n",
    "\t\tmetrics=['accuracy']\n",
    "\t)\n",
    "\n",
    "    return model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The next cell allows you to check the number of total and trainable parameters of your model and prompts a warning in case these exceeds those of a reference solution, this serves the following 3 purposes listed in order of priority:\n",
    "\n",
    "- Helps you prevent crashing the kernel during training.\n",
    "\n",
    "- Helps you avoid longer-than-necessary training times.\n",
    "- Provides a reasonable estimate of the size of your model. In general you will usually prefer smaller models given that they accomplish their goal successfully.\n",
    "\n",
    "**Notice that this is just informative** and may be very well below the actual limit for size of the model necessary to crash the kernel. So even if you exceed this reference you are probably fine. However, **if the kernel crashes during training or it is taking a very long time and your model is larger than the reference, come back here and try to get the number of parameters closer to the reference.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "editable": false,
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Save untrained model in a variable\n",
    "untrained_model = create_and_compile_model()\n",
    "\n",
    "# Check parameter count against a reference solution\n",
    "unittests.parameter_count(untrained_model)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "editable": false,
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Use it to predict the first 5 images in the train set\n",
    "predictions = untrained_model.predict(training_images[:5], verbose=False)\n",
    "\n",
    "print(f\"predictions have shape: {predictions.shape}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Expected Output:**\n",
    "\n",
    "```\n",
    "predictions have shape: (5, 10)\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "editable": false,
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Test your code!\n",
    "unittests.test_create_and_compile_model(create_and_compile_model)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise 2: EarlyStoppingCallback\n",
    "\n",
    "Now it is time to create your own custom callback. For this complete the `EarlyStoppingCallback` class and the `on_epoch_end` method in the cell below. If you need some guidance on how to proceed, check out this [link](https://www.tensorflow.org/guide/keras/writing_your_own_callbacks)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "tags": [
     "graded"
    ]
   },
   "outputs": [],
   "source": [
    "# GRADED CLASS: EarlyStoppingCallback\n",
    "\n",
    "### START CODE HERE ###\n",
    "\n",
    "# Remember to inherit from the correct class\n",
    "class EarlyStoppingCallback():\n",
    "\n",
    "    # Define the correct function signature for on_epoch_end method\n",
    "    def None():\n",
    "        \n",
    "        # Check if the accuracy is greater or equal to 0.98\n",
    "        if None >= None:\n",
    "                            \n",
    "            # Stop training once the above condition is met\n",
    "            None = None\n",
    "\n",
    "            print(\"\\nReached 98% accuracy so cancelling training!\") \n",
    "\n",
    "### END CODE HERE ###"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "editable": false,
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Test your code!\n",
    "unittests.test_EarlyStoppingCallback(EarlyStoppingCallback)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise 3: train_mnist\n",
    "\n",
    "Now that you have defined your callback it is time to complete the `train_mnist` function below. This function will receive the training data (features and targets encoded as numpy arrays) and should use it to train the model you defined earlier. It should also return the training history of the model. This object is returned when using the `fit` method of a `tf.keras.Model` as explained in the [docs](https://www.tensorflow.org/api_docs/python/tf/keras/Model#fit).\n",
    "\n",
    "**You must set your model to train for 10 epochs and the callback should fire before the 10th epoch for you to pass this part of the assignment**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "id": "rEHcB3kqyHZ6",
    "tags": [
     "graded"
    ]
   },
   "outputs": [],
   "source": [
    "# GRADED FUNCTION: train_mnist\n",
    "\n",
    "def train_mnist(training_images, training_labels):\n",
    "    \"\"\"Trains a classifier of handwritten digits.\n",
    "\n",
    "    Args:\n",
    "        training_images (numpy.ndarray): The images of handwritten digits\n",
    "        training_labels (numpy.ndarray): The labels of each image\n",
    "\n",
    "    Returns:\n",
    "        tf.keras.callbacks.History : The training history.\n",
    "    \"\"\"\n",
    "\n",
    "    ### START CODE HERE ###\n",
    "\n",
    "    # Create a compiled (but untrained) version of the model\n",
    "    # Hint: Remember you already coded a function that does this!\n",
    "    model = None\n",
    "    \n",
    "    # Fit the model for 10 epochs adding the callbacks and save the training history\n",
    "    history = None\n",
    "\n",
    "    ### END CODE HERE ###\n",
    "\n",
    "    return history"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now train the model and get the training history by calling the `train_mnist` function, passing in the appropiate parameters:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "editable": false,
    "id": "sFgpwbGly4u4",
    "tags": [
     "graded"
    ]
   },
   "outputs": [],
   "source": [
    "training_history = train_mnist(training_images, training_labels)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Expected Output:**\n",
    "\n",
    "`Reached 98% accuracy so cancelling training!` printed out before reaching 10 epochs.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "editable": false,
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Test your code!\n",
    "unittests.test_training_history(training_history)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Need more help?\n",
    "\n",
    "Run the following cell to see an architecture that works well for the problem at hand:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "editable": false,
    "tags": []
   },
   "outputs": [],
   "source": [
    "# WE STRONGLY RECOMMEND YOU TO TRY YOUR OWN ARCHITECTURES FIRST\n",
    "# AND ONLY RUN THIS CELL IF YOU WISH TO SEE AN ANSWER\n",
    "\n",
    "encoded_answer = \"CiAgIC0gQSB0Zi5rZXJhcy5JbnB1dCB3aXRoIHRoZSBzYW1lIHNoYXBlIGFzIHRoZSBpbWFnZXMKICAgLSBBIEZsYXR0ZW4gbGF5ZXIKICAgLSBBIERlbnNlIGxheWVyIHdpdGggNTEyIHVuaXRzIGFuZCBSZUxVIGFjdGl2YXRpb24gZnVuY3Rpb24KICAgLSBBIERlbnNlIGxheWVyIHdpdGggMTAgdW5pdHMgYW5kIHNvZnRtYXggYWN0aXZhdGlvbiBmdW5jdGlvbgo==\"\n",
    "encoded_answer = encoded_answer.encode('ascii')\n",
    "answer = base64.b64decode(encoded_answer)\n",
    "answer = answer.decode('ascii')\n",
    "\n",
    "print(answer)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Congratulations on finishing this week's assignment!**\n",
    "\n",
    "You have successfully implemented a callback that gives you more control over the training loop for your model. Nice job!\n",
    "\n",
    "**Keep it up!**"
   ]
  }
 ],
 "metadata": {
  "grader_version": "1",
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
